package cn.zhangfusheng.elasticsearch.scan;

import cn.zhangfusheng.elasticsearch.annotation.document.IndexDescription;
import cn.zhangfusheng.elasticsearch.annotation.document.IndexTransfer;
import cn.zhangfusheng.elasticsearch.annotation.document.field.FieldMapping;
import cn.zhangfusheng.elasticsearch.annotation.document.field.MappingParameters;
import cn.zhangfusheng.elasticsearch.annotation.dsl.DslIndex;
import cn.zhangfusheng.elasticsearch.annotation.dsl.DslParams;
import cn.zhangfusheng.elasticsearch.constant.ElasticSearchConstant;
import cn.zhangfusheng.elasticsearch.constant.enumeration.FieldType;
import cn.zhangfusheng.elasticsearch.constant.enumeration.TransferType;
import cn.zhangfusheng.elasticsearch.exception.GlobalSystemException;
import cn.zhangfusheng.elasticsearch.exception.InitRepositoryException;
import cn.zhangfusheng.elasticsearch.model.analysis.AnalysisMapping;
import cn.zhangfusheng.elasticsearch.model.analysis.vo.ReturnDetail;
import cn.zhangfusheng.elasticsearch.model.page.PageResponse;
import cn.zhangfusheng.elasticsearch.repository.ElasticSearchRepository;
import cn.zhangfusheng.elasticsearch.util.StrUtil;
import cn.zhangfusheng.elasticsearch.util.date.LocalDateTimeUtils;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.search.SearchHit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author fusheng.zhang
 * @date 2022-02-24 13:31:39
 */
@Data
public class ElasticSearchEntityRepositoryDetail {

    private final Logger log;
    /**
     * 忽略数据迁移和索引文档管理
     */
    private final boolean ignoreTransferOperation;
    /**
     * ZingRepository class
     */
    private Class<? extends ElasticSearchRepository<?>> elasticSearchRepositoryClass;
    /**
     * entity class
     */
    private Class<?> entityClass;
    /**
     * @PrimaryId 标注的字段
     */
    private Field primaryId;
    /**
     * 创建时间 isCreateTime=true
     */
    private Field createTime;
    /**
     * 更新时间 isUpdateTime=true
     */
    private Field updateTime;
    /**
     * royting 字段
     */
    private Field routing;
    /**
     * repository bean name
     */
    private String repositoryBeanName;
    /**
     * index discription
     */
    private IndexDescription indexDescription;
    /**
     * 数据迁移的index
     */
    private IndexTransfer indexTransfer;
    /**
     * 实体字段列表
     */
    private List<Field> entityDeclaredFields;
    /**
     * alias类型的字段
     */
    private List<Field> aliasFields;
    /**
     * 方法列表对应的方法参数索引
     */
    private Map<Method, Map<Integer, String>> methodParamsIndex;
    /**
     * 索引别名,默认为类名转换,例如 类TbCompany 的默认别名为tb_company
     */
    private String alias;
    /**
     * 索引名称 tb_company_v1
     */
    private String indexName;
    /**
     * es mapping
     */
    private String mapping;
    /**
     * es setting
     */
    private String settingJson;
    /**
     * 查询用的索引名称或者别名<br/>
     * 如果别名不为空,则使用别名否则使用索引
     */
    private String searchIndex;


    public ElasticSearchEntityRepositoryDetail(
            boolean ignoreTransferOperation,
            Class<? extends ElasticSearchRepository<?>> elasticSearchRepositoryClass, Class<?> entityClass,
            IndexDescription indexDescription, IndexTransfer indexTransfer, String settingJson) {
        this.log = LoggerFactory.getLogger(elasticSearchRepositoryClass);
        this.elasticSearchRepositoryClass = elasticSearchRepositoryClass;
        this.ignoreTransferOperation = ignoreTransferOperation;
        this.entityClass = entityClass;
        this.indexDescription = indexDescription;
        this.indexTransfer = indexTransfer;
        this.settingJson = settingJson;
        this.repositoryBeanName = elasticSearchRepositoryClass.getName();
        this.entityDeclaredFields = Arrays.stream(FieldUtils.getAllFields(entityClass)).filter(field -> {
            FieldMapping fieldMapping = field.getAnnotation(FieldMapping.class);
            return !(Objects.nonNull(fieldMapping) && fieldMapping.ignore());
        }).peek(field -> field.setAccessible(true)).collect(Collectors.toList());
        this.aliasFields = this.entityDeclaredFields.stream().filter(field -> {
            FieldMapping fieldMapping = field.getAnnotation(FieldMapping.class);
            return Objects.nonNull(fieldMapping) && FieldType.Alias.equals(fieldMapping.type());
        }).collect(Collectors.toList());
        // 初始化
        this.init();
        // 缓存当前对象
        ElasticSearchConstant.REPOSITORY_DETAIL_CACHE.put(this.entityClass, this);
    }

    private void init() {
        this.initAliasName();
        this.initIndexName();
        this.initOther();
    }

    private void initOther() {
        // 初始化查询的索引
        this.searchIndex = StringUtils.isNotBlank(this.alias) ? this.alias : this.indexName;
        // 初始化主键
        Optional<Field> primaryIdOption = this.entityDeclaredFields.stream().filter(o -> {
            FieldMapping fieldMapping = o.getAnnotation(FieldMapping.class);
            return Objects.nonNull(fieldMapping) && fieldMapping.primaryId();
        }).findFirst();
        this.primaryId = primaryIdOption.orElseThrow(() -> new GlobalSystemException("请设置主键"));
        // 查找创建时间和更新时间
        Optional<Field> createTimeOption = this.entityDeclaredFields.stream().filter(o -> {
            FieldMapping fieldMapping = o.getAnnotation(FieldMapping.class);
            return Objects.nonNull(fieldMapping) && fieldMapping.isCreateTime();
        }).findFirst();
        this.createTime = createTimeOption.orElse(null);
        Optional<Field> updateTimeOption = this.entityDeclaredFields.stream().filter(o -> {
            FieldMapping fieldMapping = o.getAnnotation(FieldMapping.class);
            return Objects.nonNull(fieldMapping) && fieldMapping.isUpdateTime();
        }).findFirst();
        this.updateTime = updateTimeOption.orElse(null);
        // 查找 routing 字段配置
        Optional<Field> routingOption = this.entityDeclaredFields.stream().filter(o -> {
            FieldMapping fieldMapping = o.getAnnotation(FieldMapping.class);
            return Objects.nonNull(fieldMapping) && fieldMapping.routing();
        }).findFirst();
        this.routing = routingOption.orElse(null);
        // 初始化 方法的参数索引列表
        List<Method> methods = Arrays.asList(this.elasticSearchRepositoryClass.getMethods());
        this.methodParamsIndex = new HashMap<>(methods.size());
        methods.forEach(method -> {
            Map<Integer, String> paramIndex = new HashMap<>(method.getParameterCount());
            Parameter[] parameters = method.getParameters();
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                DslParams dslParams = parameter.getAnnotation(DslParams.class);
                paramIndex.put(i, Objects.isNull(dslParams) ? String.format("arg_%s", i + 1) : dslParams.value());
            }
            this.methodParamsIndex.put(method, paramIndex);
        });
    }

    /**
     * 初始化索引别名
     */
    private void initAliasName() {
        this.alias = indexDescription.alias();
        if (StringUtils.isBlank(this.alias)) {
            this.alias = StrUtil.camelToSnakeCase(this.entityClass.getSimpleName());
        }
    }

    /**
     * 初始化indexName
     * @return
     */
    private void initIndexName() {
        String value = indexDescription.value();
        if (StringUtils.isBlank(value)) {
            value = StrUtil.camelToSnakeCase(this.entityClass.getSimpleName());
        }
        int version = indexDescription.version();
        this.indexName = String.format("%s_v%s", value, version);
    }

    /**
     * 初始化Mapping
     * MappingBuilder
     * @return
     */
    public String getMapping() {
        try {
            if (StringUtils.isBlank(this.mapping)) {
                XContentBuilder xContentBuilder = XContentFactory.jsonBuilder()
                        .startObject().startObject(ElasticSearchConstant.FIELD_PROPERTIES);
                new AnalysisMapping(this.entityDeclaredFields).builder(xContentBuilder);
                xContentBuilder.endObject().endObject().flush();
                this.mapping = xContentBuilder.getOutputStream().toString();
                log.info("\n\tindexName:{}\n\tmapping:{}", this.indexName, this.mapping);
            }
            return this.mapping;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new InitRepositoryException(e);
        }
    }

    /**
     * 解析 SearchResponse 生成返回值
     * @param searchResponse
     * @param hits
     * @param method
     * @return
     */
    public Object analysisSearchResponse(SearchResponse searchResponse, List<SearchHit> hits, Method method, int skipTotal) {
        Class<?> methodReturnType = method.getReturnType();
        // 返回值类型 0:返回集合 1:分页PageResponse -1:返回一条数据
        int resultType = -1;
        PageResponse<Object> pageResponse = null;
        if (List.class.isAssignableFrom(methodReturnType)) {
            // 返回集合
            resultType = 0;
        } else if (PageResponse.class.isAssignableFrom(methodReturnType)) {
            // 返回 分页数据
            resultType = 1;
            pageResponse = new PageResponse<>();
            pageResponse.setTotal(searchResponse.getHits().getTotalHits())
                    .setData(new ArrayList<>(hits.size())).setSkipTotal(skipTotal);
            if (!CollectionUtils.isEmpty(hits)) {
                Object[] sortValues = hits.get(hits.size() - 1).getSortValues();
                pageResponse.setSearchAfter(sortValues);
            }
        }
        // 返回单条数据验证
        if (resultType == -1 && hits.size() > 1) throw new GlobalSystemException("result length > 1");
        //
        boolean resultIsOptional = methodReturnType.equals(Optional.class);
        Class<?> returnType = methodReturnType;
        DslIndex dslIndex = method.getAnnotation(DslIndex.class);
        if (Objects.nonNull(dslIndex) && !Objects.equals(dslIndex.returnType(), Void.class)) {
            returnType = dslIndex.returnType();
        }
        if (resultIsOptional && returnType.equals(Optional.class)) {
            throw new GlobalSystemException("返回值类型为 Optional,请使用 DslIndex 配置 returnType");
        }
        // 返回值处理
        List<Object> objects = this.parseToEntity(hits, returnType);
        Object result = resultType == -1 ? objects.isEmpty() ? null : objects.get(0) : resultType == 0 ? objects : pageResponse.setData(objects);
        return resultIsOptional ? Optional.ofNullable(result) : result;
    }

    /**
     * 获取实体 的字段值
     * @param field 字段
     * @param t     实体
     * @return
     */
    private Object getFieldValue(Field field, Object t) {
        if (Objects.isNull(field) || Objects.isNull(t)) return null;
        try {
            return this.operationValue(field, field.get(t), Boolean.TRUE);
        } catch (Exception e) {
            throw new GlobalSystemException(e);
        }
    }

    /**
     * 为实体的字段赋值
     * @param field
     * @param t
     * @param value
     * @param <T>
     */
    private <T> void setFieldValue(Field field, T t, Object value) {
        try {
            Object fieldValue = this.operationValue(field, value, Boolean.FALSE);
            field.set(t, fieldValue);
        } catch (Exception e) {
            throw new GlobalSystemException(e);
        }
    }

    /**
     * entity 转 XContentBuilder
     * @param t
     * @param excludeNull 是否排除 为 null 的字段
     * @param <T>
     * @return
     */
    public <T> XContentBuilder entityToXContentBuilder(T t, boolean excludeNull) {
        try (XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject()) {
            for (Field field : this.entityDeclaredFields) {
                if (this.aliasFields.contains(field)) continue;
                Object fieldValue = getFieldValue(field, t);
                if (excludeNull) {
                    if (ObjectUtils.isNotEmpty(fieldValue)) xContentBuilder.field(field.getName(), fieldValue);
                } else {
                    if (fieldValue instanceof Collection<?>) {
                        xContentBuilder.field(field.getName(), (Collection<?>) fieldValue);
                    } else {
                        xContentBuilder.field(field.getName(), fieldValue);
                    }
                }
            }
            return xContentBuilder.endObject();
        } catch (IOException e) {
            throw new GlobalSystemException(e);
        }
    }

    /**
     * sourceMap 转 XContentBuilder
     * @param sourceMap
     * @return
     */
    public XContentBuilder mapToXContentBuilder(Map<String, Object> sourceMap) {
        try (XContentBuilder xContentBuilder = XContentFactory.jsonBuilder().startObject()) {
            for (Field field : this.entityDeclaredFields) {
                if (this.aliasFields.contains(field)) continue;
                Object fieldValue = sourceMap.get(field.getName());
                if (fieldValue instanceof Collection<?>) {
                    xContentBuilder.field(field.getName(), (Collection<?>) fieldValue);
                } else {
                    xContentBuilder.field(field.getName(), fieldValue);
                }
            }
            return xContentBuilder.endObject();
        } catch (IOException e) {
            throw new GlobalSystemException(e);
        }
    }

    /**
     * 操作值
     * @param field
     * @param fieldValue
     * @param serialize  true 序列化 false 反序列化
     * @return
     */
    private Object operationValue(Field field, Object fieldValue, boolean serialize) throws IllegalAccessException {
        if (Objects.isNull(fieldValue)) return null;
        if (LocalDateTime.class.equals(field.getType()) || Date.class.equals(field.getType())) {
            String format = null;
            MappingParameters mappingParameters = field.getAnnotation(MappingParameters.class);
            if (Objects.nonNull(mappingParameters)) {
                format = mappingParameters.format();
            } else {
                FieldMapping fieldAnnotation = field.getAnnotation(FieldMapping.class);
                if (Objects.nonNull(fieldAnnotation)) {
                    MappingParameters parameters = fieldAnnotation.mappingParameters();
                    if (Objects.nonNull(parameters) && StringUtils.isNotBlank(parameters.format())) {
                        format = parameters.format();
                    }
                }
            }
            if (Objects.nonNull(format) && serialize) {
                LocalDateTime localDateTime = LocalDateTime.class.equals(field.getType())
                        ? (LocalDateTime) fieldValue
                        : LocalDateTimeUtils.dateToLocalDateTime((Date) fieldValue);
                return LocalDateTimeUtils.format(localDateTime, format);
            }
            if (LocalDateTime.class.equals(field.getType())) {
                if (serialize) return LocalDateTimeUtils.localDateTimeToDate((LocalDateTime) fieldValue);
                return LocalDateTimeUtils.parseMatches(String.valueOf(fieldValue));
            } else {
                if (serialize) return fieldValue;
                return LocalDateTimeUtils.localDateTimeToDate(LocalDateTimeUtils.parseMatches(String.valueOf(fieldValue)));
            }
        } else {
            if (!serialize && fieldValue instanceof Map) {
                // noinspection unchecked
                return new JSONObject((Map<String, Object>) fieldValue).toJavaObject(field.getType());
            } else if (serialize) {
                if (fieldValue instanceof Collection<?> && ObjectUtils.isNotEmpty(fieldValue)) {
                    Collection<?> collection = (Collection<?>) fieldValue;
                    List<Object> result = new ArrayList<>(collection.size());
                    for (Object o : collection) {
                        if (Objects.isNull(o.getClass().getClassLoader())) {
                            result.add(o);
                        } else {
                            Field[] declaredFields = o.getClass().getDeclaredFields();
                            HashMap<String, Object> result_ = new HashMap<>(declaredFields.length);
                            for (Field declaredField : declaredFields) {
                                declaredField.setAccessible(true);
                                Object value = this.operationValue(declaredField, declaredField.get(o), Boolean.TRUE);
                                result_.put(declaredField.getName(), value);
                                declaredField.setAccessible(false);
                            }
                            result.add(result_);
                        }
                    }
                    return result;
                }
                if (Objects.nonNull(field.getType().getClassLoader())) {
                    Field[] declaredFields = field.getType().getDeclaredFields();
                    HashMap<String, Object> result = new HashMap<>(declaredFields.length);
                    for (Field declaredField : declaredFields) {
                        declaredField.setAccessible(true);
                        Object value = this.operationValue(declaredField, declaredField.get(fieldValue), Boolean.TRUE);
                        result.put(declaredField.getName(), value);
                        declaredField.setAccessible(false);
                    }
                    return result;
                }
            }
        }
        return fieldValue;
    }

    /**
     * 获取主键值
     * @param t
     * @return
     */
    public Optional<String> primaryId(Object t) {
        Object fieldValue = getFieldValue(primaryId, t);
        if (Objects.isNull(fieldValue)) return Optional.empty();
        String primaryId = String.valueOf(fieldValue);
        if (StringUtils.isBlank(primaryId)) return Optional.empty();
        return Optional.of(primaryId);
    }

    /**
     * 获取主键值 如果为空,则生成一个新的
     * @param t
     * @return
     */
    public String primaryIdIfNullSet(Object t) {
        Object fieldValue = getFieldValue(this.primaryId, t);
        String primaryId = String.valueOf(fieldValue);
        if (Objects.isNull(fieldValue) || StringUtils.isBlank(primaryId)) {
            primaryId = UUIDs.randomBase64UUID();
            setFieldValue(this.primaryId, t, primaryId);
        }
        return primaryId;
    }

    /**
     * 获取routing
     * @param t
     * @return
     */
    public String routing(Object t) {
        Object fieldValue = null;
        try {
            fieldValue = getFieldValue(routing, t);
        } catch (Exception ignored) {
        }
        return Objects.isNull(fieldValue) ? null : String.valueOf(fieldValue);
    }

    /**
     * 获取 routing
     * @param queryMap
     * @return
     */
    public String routing(Map<String, Object> queryMap) {
        if (Objects.isNull(routing) || CollectionUtils.isEmpty(queryMap) || !queryMap.containsKey(routing.getName())) {
            return null;
        }
        return String.valueOf(queryMap.get(routing.getName()));
    }

    @Override
    public String toString() {
        return String.format("\n\trepositoryBean:%s,\n\tentityBean:%s,\n\tindexName:%s,version:%s",
                repositoryBeanName, entityClass.getName(), indexName, indexDescription.version());
    }

    /**
     * 解析查询结果期
     * @param searchHits 查询结果集
     * @param queryClass 查询条件的class
     * @param <T>
     * @return
     */
    public <T> List<T> parseToEntity(List<SearchHit> searchHits, Class<?> queryClass) {
        if (CollectionUtils.isEmpty(searchHits)) return new ArrayList<>(0);
        if (!ElasticSearchConstant.RETURN_DETAIL_CACHE.containsKey(queryClass)) {
            Class<?> returnType = this.entityClass;
            DslIndex returnTypeAnnotation = queryClass.getAnnotation(DslIndex.class);
            if (Objects.nonNull(returnTypeAnnotation) && !Objects.equals(returnTypeAnnotation.returnType(), Void.class)) {
                returnType = returnTypeAnnotation.returnType();
            }
            List<Field> entityDeclaredFields = Objects.isNull(returnType) ? this.entityDeclaredFields
                    : Arrays.stream(FieldUtils.getAllFields(returnType)).peek(o -> o.setAccessible(Boolean.TRUE)).collect(Collectors.toList());
            ElasticSearchConstant.RETURN_DETAIL_CACHE.put(queryClass, new ReturnDetail(returnType, entityDeclaredFields));
        }
        ReturnDetail returnDetail = ElasticSearchConstant.RETURN_DETAIL_CACHE.get(queryClass);
        //noinspection unchecked
        return Collections.synchronizedList(searchHits).parallelStream()
                .map(s -> (T) this.parseToEntity(s.getSourceAsMap(), returnDetail.getReturnType(), returnDetail.getFields()))
                .collect(Collectors.toList());
    }

    /**
     * sourceMap 转换成 entity
     * @param source
     * @param <T>
     * @return
     */
    public <T> T parseToEntity(Map<String, Object> source) {
        return this.parseToEntity(source, this.entityClass, this.entityDeclaredFields);
    }

    /**
     * sourceMap 转 entity
     * @param source
     * @param entityClass
     * @param entityDeclaredFields
     * @param <T>
     * @return
     */
    @SuppressWarnings("unchecked")
    private <T> T parseToEntity(Map<String, Object> source, Class<?> entityClass, List<Field> entityDeclaredFields) {
        if (Objects.isNull(source)) return null;
        try {
            T newInstance = (T) entityClass.newInstance();
            entityDeclaredFields.forEach(o -> this.setFieldValue(o, newInstance, source.get(o.getName())));
            return newInstance;
        } catch (Exception e) {
            throw new GlobalSystemException(e);
        }
    }

    /**
     * 获取数据迁移类型
     * @return
     */
    public TransferType getTransgerType() {
        TransferType transferType = this.indexDescription.transferType();
        if (Objects.nonNull(this.indexTransfer)) {
            if (Objects.equals(transferType, TransferType.DEFAULT)) return this.indexTransfer.type();
        }
        return transferType;
    }

    /**
     * 为创建时间设置当前时间
     * @param t
     */
    public ElasticSearchEntityRepositoryDetail setCreateTime(Object t) {
        getNowTime(this.createTime).ifPresent(nowTime -> setFieldValue(this.createTime, t, nowTime));
        return this;
    }

    /**
     * 为更新时间创建字段
     * @param t
     */
    public void setUpdateTime(Object t) {
        getNowTime(this.updateTime).ifPresent(nowTime -> setFieldValue(this.updateTime, t, nowTime));
    }

    /**
     * 获取当前时间
     * @param field
     * @return
     */
    private Optional<Object> getNowTime(Field field) {
        if (Objects.isNull(field)) return Optional.empty();
        if (field.getType().equals(LocalDateTime.class)) return Optional.of(LocalDateTime.now());
        if (field.getType().equals(Date.class)) return Optional.of(new Date());
        throw new GlobalSystemException("目前只支持获取 LocalDateTime Date 的当前时间");
    }
}
